package com.ar4j;

import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

import com.ar4j.util.ClassUtils;
import com.ar4j.util.ObjectCache;

import net.sf.cglib.proxy.Enhancer;

/**
 * Supports the existence of named factory singletons, with a default singleton used
 * if a name is not specified. 
 */
public final class ActiveRecordFactory {
  private static ActiveRecordFactory instance;
  private static ReentrantLock instanceLock = new ReentrantLock();
  
  private static final String METADATA_CACHE_NAME = ActiveRecordFactory.class.getName() + ".metadata";
  
  /**
   * @return return an instance of the singleton factory
   */
  public static ActiveRecordFactory getFactory() {
    if(instance == null) {
      instanceLock.lock();
      try {
        if(instance == null) {
          instance = new ActiveRecordFactory();
        }
      } finally {
        instanceLock.unlock();
      }
    }
    
    return instance;
  }
  
  private ObjectCache<Class<?>, ActiveRecordMetadata> metadataCache;
  
  /**
   * Disallow outside construction
   */
  private ActiveRecordFactory() {
    this.metadataCache = new ObjectCache<Class<?>, ActiveRecordMetadata>(METADATA_CACHE_NAME, 0, 0);
  }
  
  /**
   * Creates a blank active record of the given domain class.
   */
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public <E extends ActiveRecord<E>> E getActiveRecord(Class<E> domainClass, ActiveRecordContext context) {
    ActiveRecordMetadata metadata = metadataCache.get(domainClass);
    if(metadata == null) {
      metadata = ActiveRecordMetadataFactory.getInstance().getMetadata(domainClass);
      metadataCache.put(domainClass, metadata);
    }
    
    DelegatingMethodInterceptor interceptor = new DelegatingMethodInterceptor(metadata, context);
    E dataHolder = (E) Enhancer.create(metadata.getBaseClass(), interceptor);
    interceptor.setDefaultImplementation(new DefaultActiveRecord(metadata, context, dataHolder));
    
    return dataHolder;
  }
  
  /**
   * Creates a new active record which is a copy (excluding the ID field) of the given prototype
   * active record.
   */
  public <E extends ActiveRecord<E>> E getActiveRecord(E prototype, ActiveRecordContext context) {
    return getActiveRecord(prototype, context, false);
  }
  
  /**
   * Create a new active record of the given domain class and populate it with the values from
   * the given map. The keys of the map represent property names. The values of the map will
   * be coerced to the required type before being copied into the new active record instance.
   */
  public <E extends ActiveRecord<E>> E getActiveRecord(Class<E> domainClass, Map<String, Object> values, ActiveRecordContext context) {
    E out = getActiveRecord(domainClass, context);
    for(Map.Entry<String, Object> entry : values.entrySet()) {
      Class<?> requiredType = out.getPropertyType(entry.getKey());
      Object value = ClassUtils.coerceValueToType(entry.getValue(), requiredType);
      out.setProperty(entry.getKey(), value);
    }
    
    return out;
  }
    
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
  
  /**
   * Helper for getActiveRecord which can optionally copy the ID field or not from a given
   * prototype
   */
  private <E extends ActiveRecord<E>> E getActiveRecord(E prototype, ActiveRecordContext context,  boolean copyIdField) {
    Map<String, Object> values = prototype.getPropertyMap();
    if(!copyIdField) {
      values.remove(prototype.getIdentifierPropertyName());
    }
    
    return getActiveRecord(prototype.getBaseClass(), values, context);
  }

}
